// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;

using Microsoft.TeamFoundation.Migration.Toolkit;
using Microsoft.TeamFoundation.Migration.Toolkit.Linking;

namespace Microsoft.TeamFoundation.Migration.Toolkit.Wit
{
    using MigrationFileAttachmentOperation = SyncActionContainer<IMigrationFileAttachment>;
    using LinkOperation = SyncActionContainer<ILink>;

    /// <summary>
    /// Analysis engine class; used to compare items and generate change lists for bringing them
    /// in sync.
    /// </summary>
    class AnalysisEngine
    {
        private SyncProcess m_sharedData;                   // Shared data object
        private InternalWorkItemTypes m_types;              // Cached work item types

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="sharedData">Shared data object</param>
        public AnalysisEngine(
            SyncProcess sharedData)
        {
            m_sharedData = sharedData;
            m_types = new InternalWorkItemTypes(sharedData);
        }

        /// <summary>
        /// Compares two work items and generates change lists that will bring them in sync.
        /// </summary>
        /// <param name="items">Items to compare</param>
        /// <param name="mapper">Mapped revisions information</param>
        /// <returns>Change lists</returns>
        public Pair<InternalChangeList> CompareItems(
            Pair<IMigrationWorkItem> items,
            RevisionMapper mapper)
        {
            InternalWorkItemType type = m_types[Side.Left, items.Left.WorkItemType];
            Pair<FullId> fullIds = new Pair<FullId>(
                new FullId(m_sharedData.Stores.Left, items.Left.Watermark.Id),
                new FullId(m_sharedData.Stores.Right, items.Right.Watermark.Id));

            TraceManager.TraceInformation("Comparing items '{0}' and '{1}'",
                fullIds.Left, fullIds.Right);

            // Load revisions
            List<MigrationRevision> leftRevs = LoadRevisions(items, mapper, Side.Left);
            List<MigrationRevision> rightRevs = LoadRevisions(items, mapper, Side.Right);

            IWorkItemUpdatePackage leftChanges = m_sharedData.Stores.Left.ChangeListFactory.CreateUpdatePackage(
                items.Right, items.Left);
            IWorkItemUpdatePackage rightChanges = m_sharedData.Stores.Right.ChangeListFactory.CreateUpdatePackage(
                items.Left, items.Right);
            Pair<IWorkItemUpdatePackage> changes = new Pair<IWorkItemUpdatePackage>(leftChanges, rightChanges);
            var flatFields = GetFlatFields(items);

            if (m_sharedData.IsFlat)
            {
                if (leftRevs.Count != 1 || rightRevs.Count != 1)
                {
                    throw new WitMigrationException(Resources.ErrorWrongRevisions);
                }

                // All fields are flat.
                Pair<List<MigrationField>> allFields = new Pair<List<MigrationField>>(
                    new List<MigrationField>(leftRevs[0].Fields),
                    new List<MigrationField>(rightRevs[0].Fields));

                allFields.Left.AddRange(flatFields.Left);
                allFields.Right.AddRange(flatFields.Right);
                flatFields.Left = allFields.Left;
                flatFields.Right = allFields.Right;
            }
            else
            {
                // Load revisions in the conflict analyzer
                FieldConflictAnalyzer analyzer = new FieldConflictAnalyzer(
                    type, fullIds, new Pair<List<MigrationRevision>>(leftRevs, rightRevs), mapper);

                // Synchronize revisions
                CopyRevisions(leftChanges, Side.Right, fullIds.Right.Id, rightRevs, mapper, analyzer);
                CopyRevisions(rightChanges, Side.Left, fullIds.Left.Id, leftRevs, mapper, analyzer);
            }

            // Synchronize flat fields
            SyncFlatFields(
                items, 
                null,
                changes, 
                flatFields);
                        
            // Synchronize file attachments
            bool hasAttachments = (m_sharedData.CombinedFlags & MigrationWorkItemData.Attachments) != 0
                && ((items.Left.Flags & MigrationWorkItemData.Attachments) != 0 || (items.Right.Flags & MigrationWorkItemData.Attachments) != 0);

            if (hasAttachments)
            {
                List<IMigrationFileAttachment> leftAttachments = new List<IMigrationFileAttachment>(items.Left.GetFileAttachments());
                List<IMigrationFileAttachment> rightAttachments = new List<IMigrationFileAttachment>(items.Right.GetFileAttachments());

                // Synchronize attachments
                FileAttachmentAnalyzer attachAnalyzer = FileAttachmentAnalyzer.Create(
                    m_sharedData.Session.Policies.AttachmentsConflict.ExtraComparisonAttributes,
                    new Pair<List<IMigrationFileAttachment>>(leftAttachments, rightAttachments));
                AddAttachmentsToChangeLists(fullIds, changes, attachAnalyzer);
            }

            // Synchronize links
            bool hasLinks = m_sharedData.LinkEngine != null
                && (m_sharedData.CombinedFlags & MigrationWorkItemData.Links) != 0;

            if (hasLinks)
            {
                LinkAnalyzer la = new LinkAnalyzer(
                    m_sharedData.LinkEngine,
                    m_sharedData.StoreTypes.Left,
                    new Pair<ILinkContainer>(items.Left, items.Right));

                AddLinksToChangeLists(fullIds, changes, la);
            }

            // Customization
            items.Right.OnTargetUpdate(leftChanges);
            items.Left.OnTargetUpdate(rightChanges);
            
            return new Pair<InternalChangeList>(
                new InternalChangeList(Side.Left, fullIds, leftChanges, mapper),
                new InternalChangeList(Side.Right, fullIds, rightChanges, mapper));
        }

        /// <summary>
        /// Create a change list for copying given work item to the right side.
        /// </summary>
        /// <param name="leftItem">Source item</param>
        /// <param name="mapper">Revision mapper</param>
        /// <returns>Change list</returns>
        public InternalChangeList CopyItem(
            IMigrationWorkItem leftItem,
            RevisionMapper mapper)
        {
            Pair<FullId> ids = new Pair<FullId>(
                new FullId(m_sharedData.Stores.Left, leftItem.Watermark.Id),
                null);
            TraceManager.TraceInformation("Migrating item '{0}')", ids.Left);

            InternalWorkItemType type = m_types[Side.Left, leftItem.WorkItemType];
            List<MigrationRevision> revs = new List<MigrationRevision>(leftItem.GetRevisions(
                m_sharedData.IsFlat ? leftItem.Watermark.Revision : 0));
            TraceManager.TraceInformation("{0} revision(s) obtained for item '{1}", revs.Count, ids.Left);
            IWorkItemUpdatePackage rightChanges = m_sharedData.Stores.Right.ChangeListFactory.CreateUpdatePackage(
                leftItem,
                type.Names.Right);

            var flatFields = GetFlatFields(new Pair<IMigrationWorkItem>(leftItem, null));

            if (m_sharedData.IsFlat)
            {
                // All fields are flat
                if (revs.Count != 1)
                {
                    throw new WitMigrationException(Resources.ErrorWrongRevisions);
                }
                var allFields = new List<MigrationField>(revs[0].Fields);
                allFields.AddRange(flatFields.Left);
                flatFields.Left = allFields;
            }
            else
            {
                revs.Insert(0, new MigrationRevision());        // Baseline revision

                FieldConflictAnalyzer analyzer = new FieldConflictAnalyzer(
                    type,
                    ids,
                    new Pair<List<MigrationRevision>>(revs, new List<MigrationRevision>()),
                    mapper);

                CopyRevisions(rightChanges, Side.Left, leftItem.Watermark.Id, revs, mapper, analyzer);
            }

            // Synchronize flat fields.
            SyncFlatFields(
                new Pair<IMigrationWorkItem>(leftItem, null),
                revs[0],
                new Pair<IWorkItemUpdatePackage>(null, rightChanges),
                flatFields);

            //Copy file attachments
            List<IMigrationFileAttachment> leftAttachments = new List<IMigrationFileAttachment>(leftItem.GetFileAttachments());
            FileAttachmentAnalyzer attachAnalyzer = FileAttachmentAnalyzer.Create(
                m_sharedData.Session.Policies.AttachmentsConflict.ExtraComparisonAttributes,
                new Pair<List<IMigrationFileAttachment>>(leftAttachments, new List<IMigrationFileAttachment>()));
            CopyAttachmentsToChangeList(rightChanges, attachAnalyzer);

            // Synchronize links
            bool hasLinks = m_sharedData.LinkEngine != null &&
                (m_sharedData.CombinedFlags & MigrationWorkItemData.Links) != 0;

            if (hasLinks)
            {
                LinkAnalyzer la = new LinkAnalyzer(
                    m_sharedData.LinkEngine,
                    m_sharedData.StoreTypes.Left,
                    new Pair<ILinkContainer>(leftItem, null));

                foreach (WorkItemLink l in la.ForSyncing.Left)
                {
                    rightChanges.LinkOperations.Add(
                        new SyncActionContainer<ILink>(AssociationAction.Add, l));
                }
                DeferLinks(m_sharedData.StoreTypes.Left, la.ForDeferral.Left);
            }

            // Customization
            leftItem.OnTargetUpdate(rightChanges);

            return new InternalChangeList(Side.Right, ids, rightChanges, mapper);
        }

        /// <summary>
        /// Loads all revisions necessary for detecting conflicts and for synchronizing pair of items.
        /// </summary>
        /// <param name="items">Items</param>
        /// <param name="mapper">Revision mapper object</param>
        /// <param name="side">Tells which items' revisions should be loaded</param>
        /// <returns>List of revisions</returns>
        private List<MigrationRevision> LoadRevisions(
            Pair<IMigrationWorkItem> items,
            RevisionMapper mapper,
            Side side)
        {
            IMigrationWorkItem item = items[side];
            int baseline = m_sharedData.IsFlat ? item.Watermark.Revision : mapper.Baseline[side];

            if (m_sharedData.IsFlat || baseline < item.Watermark.Revision)
            {
                return new List<MigrationRevision>(item.GetRevisions(baseline));
            }
            return new List<MigrationRevision>(0);
        }

        /// <summary>
        /// Copies revisions into the target change list.
        /// </summary>
        /// <param name="targetList">Target change list</param>
        /// <param name="sourceSide">Side source revisions belong to</param>
        /// <param name="sourceId">Id of the source work item</param>
        /// <param name="sourceRevs">Source revisions</param>
        /// <param name="mapper">Revision mapper</param>
        /// <param name="analyzer">Field conflict analyzer</param>
        private void CopyRevisions(
            IWorkItemUpdatePackage targetList,
            Side sourceSide,
            string sourceId,
            List<MigrationRevision> sourceRevs,
            RevisionMapper mapper,
            FieldConflictAnalyzer analyzer)
        {
            int iRev;
            int lastRev = mapper.LastMigrated[sourceSide];
            WitMissingUserPolicy userPolicy = m_sharedData.Session.Policies.MissingUser;

            // Skip revisions that have already been migrated
            for (iRev = 0; iRev < sourceRevs.Count; iRev++)
            {
                if (sourceRevs[iRev].Revision > lastRev)
                {
                    break;
                }
            }

            // Copy remaining revisions
            for (; iRev < sourceRevs.Count; iRev++)
            {
                MigrationRevision rev = sourceRevs[iRev];

                // Copy only non-conflicting revisions.
                if (analyzer.ConflictingRevision != null && rev.Revision >= analyzer.ConflictingRevision[sourceSide])
                {
                    break;
                }

                if (!mapper.IsSyncedRevision(sourceSide, rev.Revision))
                {
                    // Copy the revision
                    MigrationRevision targetRev = new MigrationRevision(sourceId, rev);

                    // Is the author valid for the opposite side?
                    if (!m_sharedData.Stores[!sourceSide].IsValidUser(targetRev.Author))
                    {
                        m_sharedData.Session.FireMissingUser(
                            m_sharedData.Stores[!sourceSide],
                            targetRev.Author);

                        if (userPolicy.Reaction == WitConflictReaction.Throw)
                        {
                            break;
                        }
                        else if (userPolicy.Reaction == WitConflictReaction.Default)
                        {
                            targetRev.Author = userPolicy.DefaultUser;
                        }
                    }

                    for (int i = 0; i < rev.Fields.Count; i++)
                    {
                        MigrationField f = rev.Fields[i];
                        InternalFieldDefinition fd = analyzer.WorkItemType.GetFieldDefinition(f.Name, sourceSide);

                        if (fd != null && !fd.IsFlat && analyzer.MustTakeField(sourceSide, rev.Revision, fd))
                        {
                            //Map values before adding to revision
                            object fldValue = f.Value;
                            if (fd.ValueMap != null)
                            {
                                fldValue = fd.ValueMap.GetValue(f.Value, m_sharedData.StoreTypes[sourceSide]);
                            }
                            targetRev.Fields.Add(new MigrationField(fd.Names[!sourceSide], fldValue));
                        }
                    }

                    if (targetRev.Fields.Count > 0)
                    {
                        targetList.Revisions.Add(targetRev);
                    }
                }
            }
        }

        /// <summary>
        /// Add appropriate changes to the left and right change lists.  
        /// What is added to each list will be dependent on which side is configured 
        /// as the master as well as the configured reaction to out-of-sync attachments.
        /// </summary>
        /// <param name="ids">Ids of work items being synchronized</param>
        /// <param name="changes">Changes on both sides</param>
        /// <param name="analyzer">File attachment analyzer.</param>
        private void AddAttachmentsToChangeLists(
            Pair<FullId> ids,
            Pair<IWorkItemUpdatePackage> changes,
            FileAttachmentAnalyzer analyzer)
        {
            if (analyzer.InSync == false)
            {
                WitFileAttachmentPolicy policy = m_sharedData.Session.Policies.AttachmentsConflict;
                WitConflictReaction reaction = policy.Reaction;
                SystemType masterSystemType = policy.MasterSystem;

                m_sharedData.Session.FireAttachmentConflict(m_sharedData, ids, reaction);
                if (reaction == WitConflictReaction.Throw)
                {
                    throw new WitMigrationException(Resources.ErrorAttachmentsOutOfSync);
                }

                // Determine master and slave side for File Attachments
                Side masterSide = m_sharedData.TfsSide;
                if (masterSystemType != SystemType.Tfs)
                {
                    masterSide = !m_sharedData.TfsSide;
                }
                Side slaveSide = !masterSide;

                //Add attachments from master to slave
                foreach (IMigrationFileAttachment f in analyzer.Unmatched[masterSide])
                {
                    changes[slaveSide].FileAttachmentOperations.Add(new MigrationFileAttachmentOperation(AssociationAction.Add, f));
                }

                //What to do with the unmatched attachments on the slave side is dependent 
                //on the AttachmentsConflict.Reaction setting.
                switch (reaction)
                {
                    case WitConflictReaction.Union:
                        //Add attachments from slave to master
                        foreach (IMigrationFileAttachment f in analyzer.Unmatched[slaveSide])
                        {
                            changes[masterSide].FileAttachmentOperations.Add(new MigrationFileAttachmentOperation(AssociationAction.Add, f));
                        }
                        break;
                    default: //Should be WitConflictReaction.Master... since we handled "throw" earlier.
                        //Remove attachments in slave that are not in master
                        foreach (IMigrationFileAttachment f in analyzer.Unmatched[slaveSide])
                        {
                            changes[slaveSide].FileAttachmentOperations.Add(new MigrationFileAttachmentOperation(AssociationAction.Remove, f));
                        }
                        break;
                }
            }
        }

        /// <summary>
        /// Updates changes on both sides with links.
        /// </summary>
        /// <param name="ids">Ids of work items being updated</param>
        /// <param name="changes">Changes on both sides</param>
        /// <param name="la">Link analyzer</param>
        private void AddLinksToChangeLists(
            Pair<FullId> ids,
            Pair<IWorkItemUpdatePackage> changes,
            LinkAnalyzer la)
        {
            if (!la.InSync)
            {
                WitMasterPolicy lp = m_sharedData.Session.Policies.LinksConflict;
                m_sharedData.Session.FireLinkConflict(m_sharedData, ids, lp.Reaction);

                if (lp.Reaction == WitConflictReaction.Throw)
                {
                    throw new WitMigrationException(Resources.ErrorLinksOutOfSync);
                }
                Side master = lp.MasterSystem == SystemType.Tfs ? m_sharedData.TfsSide : !m_sharedData.TfsSide;
                Side slave = !master;

                // Copy master changes to the slave
                foreach (WorkItemLink ml in la.ForSyncing[master])
                {
                    changes[slave].LinkOperations.Add(
                        new SyncActionContainer<ILink>(AssociationAction.Add, ml));
                }
                DeferLinks(m_sharedData.StoreTypes[master], la.ForDeferral[master]);

                if (lp.Reaction == WitConflictReaction.Union)
                {
                    // Copy slave changes to the master
                    foreach (WorkItemLink sl in la.ForSyncing[slave])
                    {
                        changes[master].LinkOperations.Add(
                            new SyncActionContainer<ILink>(AssociationAction.Add, sl));
                    }
                    DeferLinks(m_sharedData.StoreTypes[slave], la.ForDeferral[slave]);
                }
                else
                {
                    // Delete unmatched links
                    Debug.Assert(lp.Reaction == WitConflictReaction.Master, "Unknown reaction");
                    foreach (WorkItemLink l in la.ForSyncing[slave])
                    {
                        changes[slave].LinkOperations.Add(
                            new LinkOperation(AssociationAction.Remove, l));
                    }

                    foreach (WorkItemLink l in la.ForDeferral[slave])
                    {
                        changes[slave].LinkOperations.Add(
                            new LinkOperation(AssociationAction.Remove, l));
                    }
                }
            }
        }

        /// <summary>
        /// Add appropriate changes to the left and right change lists.  
        /// What is added to each list will be dependent on which side is configured 
        /// as the master as well as the configured reaction to out-of-sync attachments.
        /// </summary>
        /// <param name="rightChanges">List of right side changes.</param>
        /// <param name="analyzer">File attachment analyzer.</param>
        private static void CopyAttachmentsToChangeList(
            IWorkItemUpdatePackage rightChanges,
            FileAttachmentAnalyzer analyzer)
        {
            //Add attachments from left to right
            foreach (IMigrationFileAttachment f in analyzer.Unmatched.Left)
            {
                rightChanges.FileAttachmentOperations.Add(new MigrationFileAttachmentOperation(AssociationAction.Add, f));
            }
        }

        /// <summary>
        /// Defers links from the list.
        /// </summary>
        /// <param name="system">System the links belong to</param>
        /// <param name="links">List of links</param>
        private void DeferLinks(
            SystemType system,
            IList<ILink> links)
        {
            for (int i = 0; i < links.Count; i++)
            {
                m_sharedData.LinkEngine.Defer(system, links[i]);
            }
        }

        /// <summary>
        /// Obtains values of all flat fields in the given work items.
        /// </summary>
        /// <param name="items">Work items</param>
        /// <returns>Values of flat fields</returns>
        private Pair<IEnumerable<MigrationField>> GetFlatFields(
            Pair<IMigrationWorkItem> items)
        {
            Debug.Assert(items.Left != null, "Left item is null!");

            var values = new Pair<IEnumerable<MigrationField>>(null, null);
            InternalWorkItemType type = m_types[Side.Left, items.Left.WorkItemType];

            if (type.HasFlatFields)
            {
                var leftFields = new List<string>();
                var rightFields = new List<string>();
                foreach (InternalFieldDefinition def in type.GetFlatFields())
                {
                    leftFields.Add(def.Names.Left);
                    rightFields.Add(def.Names.Right);
                }

                // Process left fields
                if (leftFields.Count != 0)
                {
                    values.Left = items.Left.GetLatestValues(leftFields);
                }
                else
                {
                    values.Left = new List<MigrationField>(0);
                }

                if (items.Right != null && rightFields.Count != 0)
                {
                    values.Right = items.Right.GetLatestValues(rightFields);
                }
                else
                {
                    values.Right = new List<MigrationField>(0);
                }
            }
            else
            {
                values.Left = new List<MigrationField>(0);
                values.Right = new List<MigrationField>(0);
            }
            return values;
        }

        /// <summary>
        /// Performs synchronization of the given flat fields.
        /// </summary>
        /// <param name="items">Source work items</param>
        /// <param name="sourceRev">Source revision (flat work items only!)</param>
        /// <param name="updates">Update batches</param>
        /// <param name="fields">Fields to synchronize</param>
        private void SyncFlatFields(
            Pair<IMigrationWorkItem> items,
            MigrationRevision sourceRev,
            Pair<IWorkItemUpdatePackage> updates,
            Pair<IEnumerable<MigrationField>> fields)
        {
            InternalWorkItemType type = m_types[Side.Left, items.Left.WorkItemType];
            var ids = new Pair<FullId>(
                new FullId(m_sharedData.Stores.Left, items.Left.Watermark.Id), null);
            var revs = new Pair<FullRevision>(
                new FullRevision(ids.Left, -1), null);
            if (items.Right != null)
            {
                ids.Right = new FullId(m_sharedData.Stores.Right, items.Right.Watermark.Id);
                revs.Right = new FullRevision(ids.Right, -1);
            }
                
            // Compare fields on both sides.
            var cmp = new FieldComparer(type, fields);
            MigrationRevision targetRevision = null;
            WitMasterPolicy policy = m_sharedData.Session.Policies.FieldConflict;

            for (int i = 0; i < cmp.ConflictingFields.Count; i++)
            {
                // Notify about the changes.
                var def = cmp.ConflictingFields[i];
                if (items.Right != null)
                {
                    m_sharedData.Session.FireFieldConflict(
                        m_sharedData, def.Names.Left, revs, cmp.ConflictingValues[i], policy.Reaction);
                }

                // Resolve if necessary
                if (policy.Reaction == WitConflictReaction.Master || items.Right == null)
                {
                    Side targetSide;

                    if (items.Right == null)
                    {
                        targetSide = Side.Right;
                    }
                    else
                    {
                        targetSide = m_sharedData.TfsSide;
                        if (policy.MasterSystem == SystemType.Tfs)
                        {
                            targetSide = !targetSide;
                        }
                    }
                    if (targetRevision == null)
                    {
                        if (updates[targetSide].Revisions.Count == 0)
                        {
                            // Create a new revision.
                            if (sourceRev != null)
                            {
                                // We are syncing flat items. For the very first sync we need to establish
                                // mapping between work items, which requires knowing source revision.
                                targetRevision = new MigrationRevision(items[!targetSide].Watermark.Id, sourceRev);
                            }
                            else
                            {
                                // We are syncing regular work items. The relationship has already been
                                // established, so we do not need source revision.
                                targetRevision = new MigrationRevision(items[!targetSide].Watermark.Id, string.Empty);
                            }
                            updates[targetSide].Revisions.Add(targetRevision);
                        }
                        else
                        {
                            // Piggyback changes to the very first revision.
                            targetRevision = updates[targetSide].Revisions[0];
                        }
                    }
                    targetRevision.Fields.Add(
                        new MigrationField(def.Names[targetSide], cmp.ConflictingValues[i][!targetSide]));
                }
            }

            if (cmp.ConflictingFields.Count != 0 && items.Right != null && policy.Reaction == WitConflictReaction.Throw)
            {
                throw new WitMigrationException(Resources.ErrorWorkItemFieldsOutOfSync);
            }
        }
    }
}
